"""
三菱PLC客户端模块

支持三菱FX3U系列PLC的Modbus RTU和TCP通信
"""

import time
import logging
from typing import List, Optional
from pymodbus.client import ModbusSerialClient, ModbusTcpClient
from pymodbus.exceptions import ModbusException, ConnectionException
from pymodbus.constants import Endian
from pymodbus.payload import BinaryPayloadDecoder, BinaryPayloadBuilder

from .base import BasePLCClient

logger = logging.getLogger(__name__)


class FX3UModbusClient(BasePLCClient):
    """三菱FX3U PLC的Modbus RTU通信客户端"""
    
    # MODBUS地址映射常量
    # 位软元件地址
    # M线圈(辅助继电器): 内部中间继电器，用于程序内部逻辑控制
    # M0-M499: 非保持区域，断电后状态丢失
    # M500-M7679: 掉电保持区域，断电后状态保持
    COIL_M = 0x0000   # M0-M7679 线圈地址起始
    
    # S线圈(状态继电器): 特殊用途的内部继电器，用于步进顺控指令
    # 常用于程序的步进控制和工艺流程控制，支持保持状态
    COIL_S = 0x2000   # S0-S4095 线圈地址起始
    
    # X线圈(输入点): 物理输入点，连接到PLC的输入端子
    # 用于读取外部设备(如按钮、传感器、开关)的状态
    # 只读，不能通过程序直接改变
    COIL_X = 0x3400   # X0-X377 线圈地址起始
    
    # Y线圈(输出点): 物理输出点，连接到PLC的输出端子
    # 用于控制外部设备(如指示灯、继电器、电磁阀)
    # 可读可写，程序可以控制其状态
    COIL_Y = 0x3300   # Y0-Y377 线圈地址起始
    
    # 字软元件地址
    # D寄存器(数据寄存器): 通用数据存储，可用于存储变量、计算结果等
    # D0-D199: 非保持区域，断电后数据丢失
    # D200-D7999: 掉电保持区域，断电后数据保持(需电池正常)
    HOLDING_D = 0x0000    # D0-D7999 保持寄存器地址起始
    
    # D8寄存器(特殊数据寄存器): 系统专用寄存器，存储PLC系统状态和参数
    # 包含PLC运行状态、错误码、通信参数等信息
    # 有些只读，有些可读写，有些断电保持，有些不保持
    HOLDING_D8 = 0x1F40   # D8000-D8511 特殊寄存器地址起始
    
    # R寄存器(文件寄存器): 扩展数据存储区
    # 特点：所有R寄存器通常都支持掉电保持(需电池正常)
    # 适用于长期保存的数据、配方参数、历史记录等
    HOLDING_R = 0x2140    # R0-R32767 文件寄存器地址起始
    
    def __init__(self, port="COM3", baudrate=19200, parity='E', bytesize=8, stopbits=1, timeout=1.0, slave_id=1):
        """
        初始化Modbus RTU客户端
        
        参数:
            port (str): COM端口名称，默认为'COM3'
            baudrate (int): 波特率，默认为19200 (19.2kbps)
            parity (str): 校验位，'E'为偶校验，'O'为奇校验，'N'为无校验
            bytesize (int): 数据位，默认为8位
            stopbits (int): 停止位，默认为1位
            timeout (float): 通信超时时间，单位为秒
            slave_id (int): 从站地址，默认为1
        """
        super().__init__()
        self.port = port
        self.baudrate = baudrate
        self.parity = parity
        self.bytesize = bytesize
        self.stopbits = stopbits
        self.timeout = timeout
        self.slave_id = slave_id
        
    def connect(self) -> bool:
        """连接到PLC设备，成功返回True，失败返回False"""
        try:
            self.client = ModbusSerialClient(
                port=self.port,
                baudrate=self.baudrate,
                parity=self.parity,
                bytesize=self.bytesize,
                stopbits=self.stopbits,
                timeout=self.timeout,
                framer='rtu'
            )
            
            result = self.client.connect()
            if result:
                self.connected = True
                logger.info(f"成功连接到COM端口: {self.port}")
                return True
            else:
                self.connected = False
                logger.error(f"连接失败，请检查COM端口: {self.port}")
                return False
        except Exception as e:
            self.connected = False
            logger.error(f"连接异常: {e}")
            return False
            
    def close(self) -> None:
        """关闭到PLC设备的连接"""
        if self.client:
            self.client.close()
            self.connected = False
            logger.info("已关闭串口连接")
    
    def read_d_registers(self, start_address: int, count: int = 1) -> Optional[List[int]]:
        """
        读取D寄存器的值
        
        D寄存器掉电保持情况:
        - D0-D199: 非保持区域，断电后数据丢失
        - D200-D7999: 掉电保持区域，断电后数据保持(需电池正常)
        
        参数:
            start_address (int): 起始D寄存器地址，如D100则为100
            count (int): 要读取的寄存器数量
            
        返回:
            读取成功时返回寄存器值列表，失败时返回None
        """
        if not self.is_connected():
            logger.error("客户端未连接")
            return None
            
        try:
            # 根据地址确定使用哪个地址范围
            if 0 <= start_address < 8000:
                # 普通D寄存器
                modbus_address = self.HOLDING_D + start_address
            elif 8000 <= start_address < 8512:
                # 特殊D8寄存器
                modbus_address = self.HOLDING_D8 + (start_address - 8000)
            else:
                logger.error(f"D{start_address}超出支持的范围(0-7999)。如果您要访问40001等Modbus地址，请使用西门子S7设备类型")
                return None
            
            result = self.client.read_holding_registers(
                address=modbus_address,
                count=count,
                slave=self.slave_id
            )
            
            if result and hasattr(result, 'registers') and len(result.registers) > 0:
                logger.info(f"成功读取D{start_address}-D{start_address+count-1}: {result.registers}")
                return result.registers
            else:
                logger.error(f"读取D{start_address}失败: {result}")
                return None
                
        except ModbusException as e:
            logger.error(f"Modbus通信错误: {e}")
            return None
        except Exception as e:
            logger.error(f"读取D寄存器时发生错误: {e}")
            return None
    
    def write_d_registers(self, start_address: int, values: List[int]) -> bool:
        """
        写入D寄存器的值
        
        参数:
            start_address (int): 起始D寄存器地址
            values (List[int]): 要写入的值列表
            
        返回:
            写入成功返回True，失败返回False
        """
        if not self.is_connected():
            logger.error("客户端未连接")
            return False
            
        try:
            # 根据地址确定使用哪个地址范围
            if 0 <= start_address < 8000:
                modbus_address = self.HOLDING_D + start_address
            elif 8000 <= start_address < 8512:
                modbus_address = self.HOLDING_D8 + (start_address - 8000)
            else:
                logger.error(f"D{start_address}超出支持的范围(0-7999)")
                return False
            
            result = self.client.write_registers(
                address=modbus_address,
                values=values,
                slave=self.slave_id
            )
            
            if result:
                # 验证写入是否成功
                verify = self.read_d_registers(start_address, len(values))
                if verify and all(v1 == v2 for v1, v2 in zip(verify, values)):
                    logger.info(f"成功写入D{start_address}: {values}")
                    return True
                else:
                    logger.warning(f"写入D{start_address}后验证失败。写入值: {values}, 读取值: {verify}")
                    return True  # 实际可能写入成功
            else:
                logger.error(f"写入D{start_address}失败: {result}")
                return False
                
        except ModbusException as e:
            logger.error(f"Modbus通信错误: {e}")
            return False
        except Exception as e:
            logger.error(f"写入D寄存器时发生错误: {e}")
            return False
    
    def read_r_registers(self, start_address: int, count: int = 1) -> Optional[List[int]]:
        """读取R文件寄存器的值"""
        if not self.is_connected():
            logger.error("客户端未连接")
            return None
            
        try:
            modbus_address = self.HOLDING_R + start_address
            result = self.client.read_holding_registers(
                address=modbus_address,
                count=count,
                slave=self.slave_id
            )
            
            if result and hasattr(result, 'registers') and len(result.registers) > 0:
                logger.info(f"成功读取R{start_address}-R{start_address+count-1}: {result.registers}")
                return result.registers
            else:
                logger.error(f"读取R{start_address}失败: {result}")
                return None
                
        except ModbusException as e:
            logger.error(f"Modbus通信错误: {e}")
            return None
        except Exception as e:
            logger.error(f"读取R寄存器时发生错误: {e}")
            return None
    
    def write_r_registers(self, start_address: int, values: List[int]) -> bool:
        """写入R文件寄存器的值"""
        if not self.is_connected():
            logger.error("客户端未连接")
            return False
            
        try:
            modbus_address = self.HOLDING_R + start_address
            result = self.client.write_registers(
                address=modbus_address,
                values=values,
                slave=self.slave_id
            )
            
            if result:
                verify = self.read_r_registers(start_address, len(values))
                if verify and all(v1 == v2 for v1, v2 in zip(verify, values)):
                    logger.info(f"成功写入R{start_address}: {values}")
                    return True
                else:
                    logger.warning(f"写入R{start_address}后验证失败")
                    return True
            else:
                logger.error(f"写入R{start_address}失败: {result}")
                return False
                
        except ModbusException as e:
            logger.error(f"Modbus通信错误: {e}")
            return False
        except Exception as e:
            logger.error(f"写入R寄存器时发生错误: {e}")
            return False
    
    def read_m_coils(self, start_address: int, count: int = 1) -> Optional[List[bool]]:
        """读取M线圈的状态"""
        if not self.is_connected():
            logger.error("客户端未连接")
            return None
            
        try:
            modbus_address = self.COIL_M + start_address
            result = self.client.read_coils(
                address=modbus_address,
                count=count,
                slave=self.slave_id
            )
            
            if result and hasattr(result, 'bits') and len(result.bits) > 0:
                logger.info(f"成功读取M{start_address}-M{start_address+count-1}: {result.bits}")
                return result.bits
            else:
                logger.error(f"读取M{start_address}失败: {result}")
                return None
                
        except ModbusException as e:
            logger.error(f"Modbus通信错误: {e}")
            return None
        except Exception as e:
            logger.error(f"读取M线圈时发生错误: {e}")
            return None
    
    def write_m_coils(self, start_address: int, values: List[bool]) -> bool:
        """写入M线圈的状态"""
        if not self.is_connected():
            logger.error("客户端未连接")
            return False
            
        try:
            modbus_address = self.COIL_M + start_address
            
            if len(values) == 1:
                result = self.client.write_coil(
                    address=modbus_address,
                    value=values[0],
                    slave=self.slave_id
                )
            else:
                result = self.client.write_coils(
                    address=modbus_address,
                    values=values,
                    slave=self.slave_id
                )
            
            if result:
                verify = self.read_m_coils(start_address, len(values))
                if verify and all(v1 == v2 for v1, v2 in zip(verify, values)):
                    logger.info(f"成功写入M{start_address}: {values}")
                    return True
                else:
                    logger.warning(f"写入M{start_address}后验证失败")
                    return True
            else:
                logger.error(f"写入M{start_address}失败: {result}")
                return False
                
        except ModbusException as e:
            logger.error(f"Modbus通信错误: {e}")
            return False
        except Exception as e:
            logger.error(f"写入M线圈时发生错误: {e}")
            return False
    
    def read_x_coils(self, start_address: int, count: int = 1) -> Optional[List[bool]]:
        """读取X输入线圈的状态"""
        if not self.is_connected():
            logger.error("客户端未连接")
            return None
            
        try:
            modbus_address = self.COIL_X + start_address
            result = self.client.read_coils(
                address=modbus_address,
                count=count,
                slave=self.slave_id
            )
            
            if result and hasattr(result, 'bits') and len(result.bits) > 0:
                logger.info(f"成功读取X{start_address}-X{start_address+count-1}: {result.bits}")
                return result.bits
            else:
                logger.error(f"读取X{start_address}失败: {result}")
                return None
                
        except ModbusException as e:
            logger.error(f"Modbus通信错误: {e}")
            return None
        except Exception as e:
            logger.error(f"读取X线圈时发生错误: {e}")
            return None
    
    def read_y_coils(self, start_address: int, count: int = 1) -> Optional[List[bool]]:
        """读取Y输出线圈的状态"""
        if not self.is_connected():
            logger.error("客户端未连接")
            return None
            
        try:
            modbus_address = self.COIL_Y + start_address
            result = self.client.read_coils(
                address=modbus_address,
                count=count,
                slave=self.slave_id
            )
            
            if result and hasattr(result, 'bits') and len(result.bits) > 0:
                logger.info(f"成功读取Y{start_address}-Y{start_address+count-1}: {result.bits}")
                return result.bits
            else:
                logger.error(f"读取Y{start_address}失败: {result}")
                return None
                
        except ModbusException as e:
            logger.error(f"Modbus通信错误: {e}")
            return None
        except Exception as e:
            logger.error(f"读取Y线圈时发生错误: {e}")
            return None
    
    def write_y_coils(self, start_address: int, values: List[bool]) -> bool:
        """写入Y线圈(输出点)的状态"""
        if not self.is_connected():
            logger.error("客户端未连接")
            return False
            
        try:
            modbus_address = self.COIL_Y + start_address
            
            if len(values) == 1:
                result = self.client.write_coil(
                    address=modbus_address,
                    value=values[0],
                    slave=self.slave_id
                )
            else:
                result = self.client.write_coils(
                    address=modbus_address,
                    values=values,
                    slave=self.slave_id
                )
            
            if result:
                verify = self.read_y_coils(start_address, len(values))
                if verify and all(v1 == v2 for v1, v2 in zip(verify, values)):
                    logger.info(f"成功写入Y{start_address}: {values}")
                    return True
                else:
                    logger.warning(f"写入Y{start_address}后验证失败")
                    return True
            else:
                logger.error(f"写入Y{start_address}失败: {result}")
                return False
                
        except ModbusException as e:
            logger.error(f"Modbus通信错误: {e}")
            return False
        except Exception as e:
            logger.error(f"写入Y线圈时发生错误: {e}")
            return False
    
    def read_s_coils(self, start_address: int, count: int = 1) -> Optional[List[bool]]:
        """读取S状态线圈的状态"""
        if not self.is_connected():
            logger.error("客户端未连接")
            return None
            
        try:
            modbus_address = self.COIL_S + start_address
            result = self.client.read_coils(
                address=modbus_address,
                count=count,
                slave=self.slave_id
            )
            
            if result and hasattr(result, 'bits') and len(result.bits) > 0:
                logger.info(f"成功读取S{start_address}-S{start_address+count-1}: {result.bits}")
                return result.bits
            else:
                logger.error(f"读取S{start_address}失败: {result}")
                return None
                
        except ModbusException as e:
            logger.error(f"Modbus通信错误: {e}")
            return None
        except Exception as e:
            logger.error(f"读取S线圈时发生错误: {e}")
            return None
    
    def write_s_coils(self, start_address: int, values: List[bool]) -> bool:
        """写入S线圈(状态继电器)的状态"""
        if not self.is_connected():
            logger.error("客户端未连接")
            return False
            
        try:
            modbus_address = self.COIL_S + start_address
            
            if len(values) == 1:
                result = self.client.write_coil(
                    address=modbus_address,
                    value=values[0],
                    slave=self.slave_id
                )
            else:
                result = self.client.write_coils(
                    address=modbus_address,
                    values=values,
                    slave=self.slave_id
                )
            
            if result:
                verify = self.read_s_coils(start_address, len(values))
                if verify and all(v1 == v2 for v1, v2 in zip(verify, values)):
                    logger.info(f"成功写入S{start_address}: {values}")
                    return True
                else:
                    logger.warning(f"写入S{start_address}后验证失败")
                    return True
            else:
                logger.error(f"写入S{start_address}失败: {result}")
                return False
                
        except ModbusException as e:
            logger.error(f"Modbus通信错误: {e}")
            return False
        except Exception as e:
            logger.error(f"写入S线圈时发生错误: {e}")
            return False
    
    def read_float(self, start_address: int, wordorder=Endian.LITTLE) -> Optional[float]:
        """读取浮点数值(占用2个连续D寄存器)"""
        registers = self.read_d_registers(start_address, 2)
        if registers:
            decoder = BinaryPayloadDecoder.fromRegisters(
                registers,
                byteorder=Endian.BIG,
                wordorder=wordorder
            )
            value = decoder.decode_32bit_float()
            logger.info(f"解码浮点数: {value}")
            return value
        return None
    
    def write_float(self, start_address: int, value: float, wordorder=Endian.LITTLE) -> bool:
        """写入浮点数到D寄存器"""
        if not self.is_connected():
            logger.error("客户端未连接")
            return False
            
        try:
            builder = BinaryPayloadBuilder(wordorder=wordorder, byteorder=Endian.BIG)
            builder.add_32bit_float(value)
            registers = builder.to_registers()
            
            result = self.write_d_registers(start_address, registers)
            return result
                
        except Exception as e:
            logger.error(f"写入浮点数时发生错误: {e}")
            return False
    
    def get_device_info(self) -> dict:
        """获取设备信息"""
        info = super().get_device_info()
        info.update({
            "manufacturer": "Mitsubishi",
            "model": "FX3U",
            "communication": "Modbus RTU",
            "port": self.port,
            "baudrate": self.baudrate,
            "slave_id": self.slave_id
        })
        return info


class FX3UModbusTCPClient(FX3UModbusClient):
    """三菱FX3U PLC的Modbus TCP通信客户端"""
    
    def __init__(self, host="127.0.0.1", port=502, timeout=1.0, slave_id=1):
        """
        初始化Modbus TCP客户端
        
        参数:
            host (str): PLC的IP地址，默认为'127.0.0.1'
            port (int): Modbus TCP端口，默认为502
            timeout (float): 通信超时时间，单位为秒
            slave_id (int): 从站地址，默认为1
        """
        # 不调用父类的__init__，因为TCP和串口参数不同
        BasePLCClient.__init__(self)
        self.host = host
        self.port = port
        self.timeout = timeout
        self.slave_id = slave_id
    
    def connect(self) -> bool:
        """连接到PLC设备，成功返回True，失败返回False"""
        try:
            self.client = ModbusTcpClient(
                host=self.host,
                port=self.port,
                timeout=self.timeout
            )
            
            result = self.client.connect()
            if result:
                self.connected = True
                logger.info(f"成功连接到TCP设备: {self.host}:{self.port}")
                return True
            else:
                self.connected = False
                logger.error(f"连接失败，请检查TCP连接: {self.host}:{self.port}")
                return False
        except Exception as e:
            self.connected = False
            logger.error(f"TCP连接异常: {e}")
            return False
    
    def close(self) -> None:
        """关闭到PLC设备的连接"""
        if self.client:
            self.client.close()
            self.connected = False
            logger.info(f"已关闭TCP连接: {self.host}:{self.port}")
    
    def get_device_info(self) -> dict:
        """获取设备信息"""
        # 直接调用BasePLCClient的方法，跳过FX3UModbusClient
        from .base import BasePLCClient
        info = BasePLCClient.get_device_info(self)
        info.update({
            "manufacturer": "Mitsubishi",
            "model": "FX3U",
            "communication": "Modbus TCP",
            "host": self.host,
            "tcp_port": self.port,
            "slave_id": self.slave_id
        })
        return info
